/*
 * Copyright (c) 2012-2017 Red Hat, Inc.
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License v1.0
 * which accompanies this distribution, and is available at
 * http://www.eclipse.org/legal/epl-v10.html
 *
 * Contributors:
 *   Red Hat, Inc. - initial API and implementation
 */
package org.eclipse.che.plugin.openshift.client.kubernetes;

import java.util.HashMap;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.apache.commons.lang.StringUtils;
import org.eclipse.che.plugin.docker.client.json.ContainerConfig;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * Converter of labels defined in {@link ContainerConfig} for matching to Kubernetes annotation
 * requirements
 */
public final class KubernetesLabelConverter {
  private static final Logger LOG = LoggerFactory.getLogger(KubernetesLabelConverter.class);
  /** Prefix used for che server labels */
  private static final String CHE_SERVER_LABEL_PREFIX = "che:server";
  /** Padding to use when converting server label to DNS name */
  private static final String CHE_SERVER_LABEL_PADDING = "0%s0";
  /**
   * Regex to use when matching converted labels -- should match {@link CHE_SERVER_LABEL_PADDING}
   */
  private static final Pattern CHE_SERVER_LABEL_KEY = Pattern.compile("^0(.*)0$");

  private static final String KUBERNETES_ANNOTATION_REGEX =
      "([A-Za-z0-9][-A-Za-z0-9_\\.]*)?[A-Za-z0-9]";

  private KubernetesLabelConverter() {}

  /** @return prefix that is used for Che server labels */
  public static String getCheServerLabelPrefix() {
    return CHE_SERVER_LABEL_PREFIX;
  }

  /**
   * Converts a map of labels to match Kubernetes annotation requirements. Annotations are limited
   * to alphanumeric characters, {@code '.'}, {@code '_'} and {@code '-'}, and must start and end
   * with an alphanumeric character, i.e. they must match the regex {@code
   * ([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9]}
   *
   * <p>Note that entry keys should begin with {@link
   * KubernetesLabelConverter#CHE_SERVER_LABEL_PREFIX} and entries should not contain {@code '.'} or
   * {@code '_'} before conversion; otherwise label will not be converted and included in output.
   *
   * <p>This implementation is relatively fragile -- changes to how Che generates labels may cause
   * this method to stop working. In general, it will only be possible to convert labels that are
   * alphanumeric plus up to 3 special characters (by converting the special characters to {@code
   * '_'}, {@code '-'}, and {@code '.'} as necessary).
   *
   * @param labels Map of labels to convert
   * @return Map of labels converted to DNS Names
   */
  public static Map<String, String> labelsToNames(Map<String, String> labels) {
    Map<String, String> names = new HashMap<>();
    if (labels == null) {
      return names;
    }
    for (Map.Entry<String, String> label : labels.entrySet()) {

      if (!hasConversionProblems(label)) {

        String key = label.getKey();
        String value = label.getValue();

        // Convert keys: e.g. "che:server:4401/tcp:ref" ->
        // "che.server.4401-tcp.ref"
        key = convertLabelKey(key);
        // Convert values: e.g. "/api" -> ".api" -- note values may
        // include '-' e.g. "tomcat-debug"
        value = convertLabelValue(value);

        // Add padding since DNS names must start and end with
        // alphanumeric characters
        key = addPadding(key);
        value = addPadding(value);

        if (matchesKubernetesLabelRegex(key) && matchesKubernetesLabelRegex(value)) {
          names.put(key, value);
        } else {
          LOG.error(
              "Could not convert label {} into Kubernetes annotation: labels must be alphanumeric with ':' and '/'",
              label.toString());
        }
      }
    }
    return names;
  }

  /**
   * Undoes the label conversion done by {@link KubernetesLabelConverter#labelsToNames(Map)}
   *
   * @param labels Map of DNS names
   * @return Map of unconverted labels
   */
  public static Map<String, String> namesToLabels(Map<String, String> names) {
    Map<String, String> labels = new HashMap<>();
    if (names == null) {
      return labels;
    }
    for (Map.Entry<String, String> entry : names.entrySet()) {
      String key = entry.getKey();
      String value = entry.getValue();

      // Remove padding
      Matcher keyMatcher = CHE_SERVER_LABEL_KEY.matcher(key);
      Matcher valueMatcher = CHE_SERVER_LABEL_KEY.matcher(value);
      if (!keyMatcher.matches() || !valueMatcher.matches()) {
        continue;
      }
      key = keyMatcher.group(1);
      value = valueMatcher.group(1);

      // Convert key: e.g. "che.server.4401_tcp.ref" -> "che:server:4401/tcp:ref"
      key = key.replaceAll("\\.", ":").replaceAll("_", "/");
      // Convert value: e.g. Convert values: e.g. "_api" -> "/api"
      value = value.replaceAll("_", "/");

      labels.put(key, value);
    }
    return labels;
  }

  /**
   * Checks if there are any potential problems coupled with label conversion
   *
   * @param label
   * @return true if label has no conversion issues, false otherwise
   */
  private static boolean hasConversionProblems(final Map.Entry<String, String> label) {
    boolean hasProblems = false;
    String key = label.getKey();
    String value = label.getValue();

    if (StringUtils.isBlank(value)) {
      LOG.error("The label {} is blank", label.toString());
      hasProblems = true;
    } else if (key.contains(".") || key.contains("_") || value.contains("_")) {
      LOG.error(
          "Cannot convert label {} to DNS Name: '-' and '.' are used as escape characters",
          label.toString());
      hasProblems = true;
    } else if (!key.startsWith(CHE_SERVER_LABEL_PREFIX)) {
      LOG.warn(
          "Expected CreateContainerParams label key {} to start with {}",
          key,
          CHE_SERVER_LABEL_PREFIX);
    }
    return hasProblems;
  }

  /** Convert keys: e.g. "che:server:4401/tcp:ref" -> "che.server.4401-tcp.ref" */
  private static String convertLabelKey(final String key) {
    return key.replaceAll(":", ".").replaceAll("/", "_");
  }

  /** Convert values: e.g. "/api" -> ".api" Note: values may include '-' e.g. "tomcat-debug" */
  private static String convertLabelValue(final String value) {
    return value.replaceAll("/", "_");
  }

  /** Adds padding since DNS names must start and end with alphanumeric characters */
  private static String addPadding(final String label) {
    return String.format(CHE_SERVER_LABEL_PADDING, label);
  }

  private static boolean matchesKubernetesLabelRegex(final String label) {
    return label.matches(KUBERNETES_ANNOTATION_REGEX);
  }
}
